package jmine.tec.environment.utils;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;

import jmine.tec.persist.annotation.View;
import jmine.tec.utils.io.IOUtils;

import org.apache.commons.lang.StringUtils;
import org.hibernate.EntityMode;
import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;

import bancosys.tec.utils.db.descriptor.FlatReferenceDatabaseBuilder.ReferenceDatabaseDescriptionType;
import bancosys.tec.utils.db.descriptor.ReferenceDatabaseEntry;
import bancosys.tec.utils.md5sum.MD5SumTool;

/**
 * Classe utilitária para otimização do processo de build.
 * 
 * @author Cesar Olmos
 */
public final class BuildData {

    private static final String MD5_EXTENSION = ".md5";

    private static String dataDir;

    static {
        setDataDir(System.getProperty("user.home") + File.separator + ".jmine-build");
    }

    /**
     * Contrutor privado.
     */
    private BuildData() {
        super();
    }

    /**
     * Salva um arquivo de MD5.
     * 
     * @param key a chave do arquivo.
     * @param md5 o md5 calculado.
     */
    public static void saveMD5(String key, String md5) {
        String filename = dataDir + key + MD5_EXTENSION;
        try {
            OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filename), "ASCII");
            osw.write(md5);
            osw.flush();
            osw.close();
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Não foi possível salvar o arquivo de md5 " + filename, e);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("Não foi possível salvar o arquivo de md5 " + filename, e);
        } catch (IOException e) {
            throw new RuntimeException("Não foi possível salvar o arquivo de md5 " + filename, e);
        }
    }

    /**
     * Recupera o valor de um MD5 salvo previamente em um arquivo.
     * 
     * @param key a chave do arquivo.
     * @return o md5 salvo previamente ou <code>null</code> caso o arquivo não seja encontrado.
     */
    public static String loadMD5(String key) {
        String filename = dataDir + key + MD5_EXTENSION;
        try {
            File file = new File(filename);
            if (!file.exists()) {
                return null;
            }
            BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "ASCII"));
            String md5 = br.readLine();
            if (br.ready()) {
                throw new RuntimeException("O arqivo de md5 " + filename + " possuí mais de uma linha.");
            }
            br.close();
            return md5;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Não foi possível ler o arqivo de md5 " + filename, e);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("Não foi possível ler o arqivo de md5 " + filename, e);
        } catch (IOException e) {
            throw new RuntimeException("Não foi possível ler o arqivo de md5 " + filename, e);
        }
    }

    /**
     * Define o diretório a ser utilizado para guardar informações de build.
     * 
     * @param newDataDir o diretório.
     */
    public static void setDataDir(String newDataDir) {
        File file = new File(newDataDir);
        if (!file.exists()) {
            if (!file.mkdirs()) {
                throw new RuntimeException("Não foi possível criar diretório para build '" + newDataDir + "'.");
            }
        } else if (!file.isDirectory()) {
            throw new RuntimeException("O diretório para build especificado (" + newDataDir + ") não é um diretório.");
        } else if (!file.canWrite() && !file.canRead()) {
            throw new RuntimeException("O diretório para build especificado (" + newDataDir + ") não pode ser leito ou escrito.");
        }

        dataDir = file.getAbsolutePath() + File.separator;
    }

    /**
     * devolve o diretório utilizado atualmente para guardar informações de build.
     * 
     * @return o diretório.
     */
    public static String getDataDir() {
        return dataDir;
    }

    /**
     * Devolve a chave que deve ser utilizada para o schema de banco de dados.
     * 
     * @param environmentKey a chave do ambiente.
     * @return a chave.
     */
    public static String getSchemaKey(String environmentKey) {
        return environmentKey + "_schema";
    }

    /**
     * Devolve a chave que deve ser utilizada para base de referência.
     * 
     * @param environmentKey a chave do ambiente.
     * @param databaseServerType o tipo de banco de dados.
     * @param type o tipo de dados utilizados.
     * @return a chave.
     */
    public static String getDataKey(String environmentKey, String databaseServerType, ReferenceDatabaseDescriptionType type) {
        return environmentKey + "_" + databaseServerType + "_" + type + "_data";
    }

    /**
     * Gera o MD5 do schema representando pleas classes e os schemas extras definidos nela.
     * 
     * @param schemaStatements schemaStatements
     * @param extraSchemas os schemas extras.
     * @return o MD5 gerado.
     */
    public static String generateSchemaMD5(String[] schemaStatements, List<ReferenceDatabaseEntry> extraSchemas) {
        try {
            InputStream[] iss = new InputStream[1 + extraSchemas.size()];

            iss[0] = new ByteArrayInputStream(StringUtils.join(schemaStatements).getBytes());

            int offset = 1;
            for (int i = 0; i < extraSchemas.size(); i++) {
                iss[offset + i] = BuildData.class.getClassLoader().getResourceAsStream(extraSchemas.get(i).getClasspathResourcePath());
            }

            InputStream is = IOUtils.append(iss);
            String md5 = MD5SumTool.md5Sum(is);
            is.close();
            return md5;
        } catch (IOException e) {
            throw new RuntimeException("Erro calculando MD5.", e);
        }
    }

    /**
     * Gera o MD5 de todas as entradas da base de referência.
     * 
     * @param entries as entradas da base de referência.
     * @param schemaMD5 o MD5 do schema utilizado por essa base de referência.
     * @return o MD5 gerado.
     */
    public static String generateDataMD5(List<ReferenceDatabaseEntry> entries, String schemaMD5) {
        try {
            InputStream[] iss = new InputStream[entries.size() + 1];
            for (int i = 0; i < entries.size(); i++) {
                InputStream input = BuildData.class.getClassLoader().getResourceAsStream(entries.get(i).getClasspathResourcePath());
                if (input == null) {
                    throw new RuntimeException("O arquivo " + entries.get(i).getClasspathResourcePath() + " nao existe no classpath.");
                }
                iss[i] = input;
            }
            iss[entries.size()] = new ByteArrayInputStream(schemaMD5.getBytes("ASCII"));

            InputStream is = IOUtils.append(iss);
            String md5 = MD5SumTool.md5Sum(is);
            is.close();
            return md5;
        } catch (IOException e) {
            throw new RuntimeException("Erro calculando MD5.", e);
        }
    }

    /**
     * Retorna as classes mapeadas, excluindo aquelas anotadas com
     * 
     * @View.
     * @param sf a session factory.
     * @return List de class
     */
    public static List<Class<?>> getMappedClasses(SessionFactory sf) {
        List<Class<?>> mappedClasses = new ArrayList<Class<?>>();
        @SuppressWarnings("unchecked")
        Collection<ClassMetadata> meta = sf.getAllClassMetadata().values();
        for (ClassMetadata classMetadata : meta) {
            Class<?> mappedType = classMetadata.getMappedClass(EntityMode.POJO);
            if (!mappedType.isAnnotationPresent(View.class)) {
                mappedClasses.add(mappedType);
            }
        }
        return mappedClasses;
    }

    /**
     * Retorna as classes mapeadas, excluindo aquelas anotadas com
     * 
     * @View.
     * @param sf a session factory.
     * @return List de class
     */
    public static List<Class<?>> getMappedClassesIncludingSuperclasses(SessionFactory sf) {
        List<Class<?>> mappedClasses = getMappedClasses(sf);

        includeMappedSuperclasses(mappedClasses);
        return mappedClasses;
    }

    /**
     * Adiciona à lista as superclasse das classes mapeadas que também sejam entidades ou sejam mapped superclasses.
     * 
     * @param mappedClasses mappedClasses
     */
    protected static void includeMappedSuperclasses(List<Class<?>> mappedClasses) {
        List<Class<?>> added = new ArrayList<Class<?>>();
        for (Class<?> mapped : mappedClasses) {
            Class<?> superClass = mapped.getSuperclass();
            while (superClass != null
                    && (superClass.isAnnotationPresent(MappedSuperclass.class) || superClass.isAnnotationPresent(Entity.class))) {
                if (!mappedClasses.contains(superClass) && !added.contains(superClass)) {
                    added.add(superClass);
                }
                superClass = superClass.getSuperclass();
            }
        }
        mappedClasses.addAll(added);
    }
}
